Consume a C++/WinRT component in a C# desktop application
Introduction
I have a function called ‘GetDescriptiveStatistics’ that takes some data and returns summary statistics. The thing is, the function is written in C++ (using STL and Boost), and I want to call it and display the results in a Windows desktop application written in C# using XAML and WinUI3.0.
If you know how to do this, you can stop reading here and take a beach break. If not, this post describes the steps to take to write a WinRT (Windows Runtime) Component that exposes the ‘GetDescriptiveStatistics’ functionality, generate a C# projection (interop), and finally consume it in a C# desktop application allowing me to bind the results to a WinUI3.0 ListView.
Why would you want to do this?
Based on the Microsoft Learn article Windows Runtime components with C++/WinRT there are three main reasons for building a Windows Runtime component in C++/WinRT.
- To enjoy the performance advantage of C++ in complex or computationally intensive operations.
- To reuse standard C++ code that’s already written and tested.
- To expose Win32 functionality to a Universal Windows Platform (UWP) app written in, for example, C#.
In this particular case, it is the second and third reasons that interested me. In the past, you could ‘connect’ a native C++ codebase to .NET by creating a COM wrapper component (perhaps using ATL/MFC), or you could create a custom managed wrapper using C++/CLI. The approach adopted here (as well as the older approach) is demonstrated in the suite SoftwareInteroperability under the three projects: StatisticsLibrary, StatisticsLibraryWRC, and StatisticsLibraryProjection.
Stages
1. Build a static library
The first stage is to build the C++ code into a Universal Windows C++ static library project.
This is really a convenience. It allows us to keep the native C++ code separate from the C++/WinRT code. And this means we can write C++ unit tests directly against this library without having to worry about the details of WinRT types. The resulting StatisticsLibrary consists of some basic statistical functions (descriptive statistics and linear regression) and statistical tests (T-test, F-test and ANOVA). The StatisticsLibrary.h
file contains the declaration of the function ‘GetDescriptiveStatistics’ as follows:
The function takes some data and optionally a list of keys corresponding to the summary statistics that you want to get. If it is empty, the function returns all the summary statistics (mean, median, standard deviation, variance and so on). The summary statistics are returned as a set of key-value pairs.
2. Create a Windows Runtime Component
The next stage is to create a StatisticsLibraryWRC project using the ‘Windows Runtime Component (C++/WinRT)’ Visual Studio project template. This project references the StatisticsLibrary static library. The StatisticsLibraryWRC is the interop layer. Here we need to do two things.
Firstly we need to define the API layer. The API is specified using IDL MIDL3.0. The one we are interested in is defined in the Statistics.idl
file under the Components
folder. The API defines the name ‘DescriptiveStatistics’ and the WinRT types. The WinRT types we select correspond closely to the native C++ types. For the data parameter we pass in an IVector<Double>
, and optionally an IVector<String>
for the keys. For the return type we choose to use IMap<String, Double>
.
With the IDL complete, we can build the project. This will fail, but the cppwinrt.exe
will usefully generate stubs for the API defined by the IDL. We can take this stub code from the generated .h
and .cpp
files and copy it to the project source location. Then we can write the implementation code. This simply forwards the call DescriptiveStatistics
to the native C++ function GetDescriptiveStatistics
and deals with the returned results.
The second main task in the interop layer is to write code to perform any conversions from C++ (STL) types to WinRT/C++ types (and vice-versa). The two functions in the Conversions
namespace perform the conversion from IVector<double>
to a std::vector<double>
and ResultsToMap
converts a std::unordered_map<std::string, double>
to an IMap<hstring, double>
.
At this point, we can build the complete Windows Runtime Component.
3. Create a C# projection
The next stage is to create a project that we use to generate a C# projection based on our C++/WinRT component. For this we create StatisticsLibraryProjection. This is a C# class library project targeting .NET 6.0. We need to add the Microsoft.Windows.CsWinRT
package to the project using the NuGet package manager. And we need to add a project reference to the StatisticsLibraryWRC project. The screenshot below shows the project dependencies.
We get rid of the boilerplate Class1.cs
file and we add a NuGet spec (.nuspec
) file to the StatisticsLibraryProjection project under a ‘nuget’ folder. The .nuspec
file is a standard XML file renamed StatisticsLibraryProjection.nuspec
. The .nuspec
file contains the metadata and targets required by NuGet. Finally, we configure the StatisticsLibraryProjection.csproj
file to generate the NuGet package for the correct target(s) and in the right directory. These properties specify the NuspecFile and the directory to generate the NuGet package. This is more fully described in the article Generate a C# projection …
After the project is built, the component WinMD and the implementation assembly (StatisticsLibraryWRC.winmd
and StatisticsLibraryWRC.dll
), the projection source files, and the projection assembly (StatisticsLibraryProjection.dll
), will all be generated under the _build output directory. You’ll also be able to see the generated NuGet package, StatisticsLibraryWRC.0.1.0-prerelease.nupkg
, under the \StatisticsLibraryProjection\nuget
folder.
4. Consume the Windows Runtime Component in C#
Now we are ready to use the NuGet package in a C# .NET 6 application. We simply add a reference to the package to the StatisticsViewerWinUI project using the NuGet package manager (ensuring that the package sources are correctly set).
The StatisticsViewerWinUI project is based on the Visual Studio project template for C# ‘Blank App, Packaged (WinUI 3 in Desktop)’. The application consists of a single main view (defined in MainPage.xaml
). This contains the menus and the list view controls. The ListView on the right handles displaying the results returned from the call to ‘GetDescriptiveStatistics’ as shown below.
At this point it would be really nice to say, ‘well, we can set the XAML ListView ItemsSource property to bind directly to the DescriptiveStatistics results. After all, via the projection, they are being returned as an IDictionary<string, double>
’. Unfortunately, I couldn’t get this to work. The ListView binding seems to require an ObservableCollection<...>
. Therefore, in the MainViewModel.cs
we declare a Results
property that sets/gets an observable collection of ResultStatistics
, as follows:
In the XAML we configure the ListView ItemsSource
to bind to this collection. Additionally, the ListView HeaderTemplate
is specified as having two columns and the ListView ItemTemplate
is defined as consisting of a ResultStatistic
item which in turn exposes properties to get the name and value.
In the MainViewModel.cs
we define a function PopulateResults
which takes the IDictionary<string, double>
returned from the call to DescriptiveStatistics
and iteratively creates ResultStatistic
items from this. These are then notified to the ObservableCollection<>
which updates the ListView using the ItemTemplate
defined in the MainPage.xaml
. The result is that we can populate the ListView (relatively) easily. A number of the other classes in the StatisticsLibrary return their results in this form, so we don’t need to do anything else.
And that is all there is to it. We have successfully connected native C++ code to .NET via C++/WinRT, and consumed the Windows Runtime Component in a C# Windows desktop application.
Wrap up
So why is this useful? Well apart from solving the original problem, it provides a generalizable stack. I can wrap (high-performance) C++ code using WinRT types in order to create an interop component, generate a C# projection, distribute the component alongside the projection assembly as a NuGet package and finally consume the NuGet package from a .NET application.
References
Windows Runtime components with C++/WinRT
Generate a C# projection from a C++/WinRT component, distribute as a NuGet for .NET apps
Comments